package org.melanesia.sql;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import org.melanesia.converters.JavaToJdbcConverter;
import org.melanesia.sql.utils.NamedParametersHelper;

/**
 * Pagination support abstract class.
 *
 * This class should be subclassed, and {@link #prepare()} method should be
 * implemented, inside of which wrapped sql and additional parameters should be
 * registered using {@link #setSql(SQLVariation, String)} and
 * {@link #setParameter(SQLVariation, String, Object)} for each
 * {@link SQLVariation}.
 *
 * This data is lated used inside {@link org.melanesia.sql.Query} for
 * query-based ranging and pagination.
 *
 * @author marcin.kielar
 *
 */
public abstract class PaginationSupport {

    /**
     * SQL variation. This enumeration defines all possible variations of SQL
     * needed by the {@link org.melanesia.sql.Query} class in order to perform
     * query-based pagination.
     */
    public static enum SQLVariation {

        /**
         * For fetching only a defined range of rows from the original query
         * result set. The range is defined by {@link RangeCallback} which can
         * be obtained using {@link PaginationSupport#getStartRow()} and
         * {@link PaginationSupport#getStopRow()}.
         */
        RANGED_QUERY,

        /**
         * For calculating total rows returned by the original sql query.
         */
        COUNT_QUERY;
    }

    /**
     * Boundary type.
     * @see PaginationSupport#getBoundaryType
     */
    public static enum BoundaryType {

        /**
         * When {@link PaginationSupport#rangeCallback} defines a range that is
         * bounded from both top and bottom.
         */
        RANGE,

        /**
         * When {@link PaginationSupport#rangeCallback} defines a range that is
         * bounded from bottom only, and thus only top rows are returned.
         */
        TOP_ROWS,

        /**
         * When {@link PaginationSupport#rangeCallback} defines a range that is
         * bounded from top only, and thus only bottom rows are returned.
         */
        BOTTOM_ROWS;
    }

    /**
     * Original sql of the {@link org.melanesia.sql.Query} object that is being
     * handled with this {@code PaginationSupport}.
     */
    private String originalSql;


    /**
     * Range callback defining how this {@code PaginationSupport} object should
     * behave.
     */
    private RangeCallback rangeCallback;

    /**
     * Collection of wrapped sql for different scenarios.
     *
     * @see SQLVariation
     * @see #setSql(SQLVariation, String)
     * @see #getSql(SQLVariation)
     */
    private final Map<SQLVariation, String> wrappedSql = new HashMap<SQLVariation, String>();

    /**
     * Collection of additional query parameters for sql variations.
     *
     * @see SQLVariation
     * @see #setParameter(SQLVariation, String, Object)
     * @see #setParameter(SQLVariation, String, Object, JavaToJdbcConverter)
     * @see #getAdditionalParameters(SQLVariation)
     */
    private final Map<SQLVariation, Collection<InputParameter>> additionalParameters =
            new HashMap<SQLVariation, Collection<InputParameter>>();
    {
        additionalParameters.put(SQLVariation.RANGED_QUERY, new ArrayList<InputParameter>());
        additionalParameters.put(SQLVariation.COUNT_QUERY, new ArrayList<InputParameter>());
    };

    /**
     * Collection of ignored columns for sql variations.
     *
     * @see SQLVariation
     * @see #ignoreColumn(SQLVariation, String)
     * @see #isColumnIgnored(SQLVariation, String)
     */
    private final HashMap<SQLVariation, Collection<String>> ignoredColumns = new HashMap<SQLVariation, Collection<String>>();
    {
        ignoredColumns.put(SQLVariation.RANGED_QUERY, new HashSet<String>());
        ignoredColumns.put(SQLVariation.COUNT_QUERY, new HashSet<String>());
    };

    /**
     * Initialization method. This is a framework method, that is being called
     * from inside of the {@link org.melanesia.sql.Query} class, whenever there
     * is a need for using a {@code PaginationSupport} functionality.
     *
     * @param originalSql
     *            original sql of the {@link org.melanesia.sql.Query} object
     *            that is being handled with this {@code PaginationSupport}
     *            object.
     *
     *
     * @param rangeCallback
     *            range callback defining pagination
     * @return this object
     */
    final PaginationSupport initialize(final String originalSql, final RangeCallback rangeCallback) {
        this.originalSql = originalSql;
        this.rangeCallback = rangeCallback;

        prepare();

        return this;
    }

    /**
     * Returns original sql of the {@link org.melanesia.sql.Query} object that
     * is being handled with this {@code PaginationSupport} object.
     *
     * @return original sql query
     */
    protected final String getOriginalSql() {
        return originalSql;
    }

    /**
     * Returns first row-to-return number, as defined inside a
     * {@link RangeCallback}. Concrete implementations of this class should use
     * this information to create a wrapped range query and pass it as an
     * additional parameter to this wrapped query.
     *
     * @see RangeCallback
     * @see org.melanesia.sql.Query#setRangeCallback(RangeCallback)
     *
     * @return first row number
     */
    protected final long getStartRow() {
        return rangeCallback.getStartRow();
    }

    /**
     * Returns last row-to-return number, as defined inside a
     * {@link RangeCallback}. Concrete implementations of this class should use
     * this information to create a wrapped range query and pass it as an
     * additional parameter to this wrapped query.
     *
     * @see RangeCallback
     * @see org.melanesia.sql.Query#setRangeCallback(RangeCallback)
     *
     * @return last row number
     */
    protected final long getStopRow() {
        return rangeCallback.getStopRow();
    }

    /**
     * Registers a wrapped query variation for execution during
     * {@link org.melanesia.sql.Query} usage.
     *
     * @param variation
     *            sql query variation
     * @param sql
     *            wrapped query to register
     */
    protected final void setSql(final SQLVariation variation, final String sql) {
        wrappedSql.put(variation, sql);
    }

    /**
     * Returns a wrapped query variation for execution during
     * {@link org.melanesia.sql.Query} usage.
     *
     * @param variation
     *            sql variation
     * @return wrapped statement for given variation
     */
    final String getSql(final SQLVariation variation) {
        return wrappedSql.get(variation);
    }

    /**
     * Generates input parameters names that are unique in the context of this
     * query - i.e. it is guaranteed they will not collide with existing query
     * parameters. This is a helper method which generates additional parameter
     * names, when wrapping original {@link org.melanesia.sql.Query} sql for
     * range / count execution.
     *
     * This method takes a mandatory {@code prefix} argument ands add integers
     * to it, until it finds the concatenated string makes a unique parameter
     * name.
     *
     * @param prefix
     *            a prefix for parameter name
     *
     * @param parameterCount
     *            number of parameter names to generate
     * @return an array of generated unique parameter names
     */
    protected final String[] generateUniqueParameterNames(final String prefix, final int parameterCount) {
        return new NamedParametersHelper(originalSql).generateUniqueNamedParameterNames(prefix, parameterCount);
    }

    /**
     * Returns a collection of additional parameters, registered with
     * {@link #setParameter(SQLVariation, String, Object)}.
     *
     * @param variation
     *            sql variation
     * @return collection of additional query parameters for given sql variation
     */
    final Collection<InputParameter> getAdditionalParameters(final SQLVariation variation) {
        return additionalParameters.get(variation);
    }

    /**
     * Sets the query additional parameter value.
     * Parameters set by this method are dedicated for range and count query variations, and are applied only in those situations.
     * For passing normal query parameters, use {@link org.melanesia.sql.Query#setParameter(String, Object)}.
     *
     * @see org.melanesia.sql.Query#setParameter(String, Object)
     *
     * @param variation
     *            sql variation
     * @param name
     *            parameter name
     * @param value
     *            parameter value
     */
    protected final void setParameter(final SQLVariation variation, final String name, final Object value) {
        additionalParameters.get(variation).add(new InputParameter(name, value));
    }

    /**
     * Sets the query additional parameter value.
     * Parameters set by this method are dedicated for range and count query variations, and are applied only in those situations.
     * For passing normal query parameters, use {@link org.melanesia.sql.Query#setParameter(String, Object, JavaToJdbcConverter)}.
     *
     * @see org.melanesia.sql.Query#setParameter(String, Object, JavaToJdbcConverter)
     *
     * @param variation
     *            sql variation
     * @param name
     *            parameter name
     * @param value
     *            parameter value
     * @param converter
     *          java to jdbc converter
     */
    protected final void setParameter(final SQLVariation variation, final String name, final Object value,
            final JavaToJdbcConverter converter) {
        additionalParameters.get(variation).add(new InputParameter(name, value, converter));
    }

    /**
     * Tells melanesia to ignore given column from the result set, during
     * mapping to result bean. For such column, no property accessor is looked
     * up, and no actions are taken to map the column to the bean's property.
     * This is an addition to the
     * {@link org.melanesia.sql.Query#ignoreColumn(String)} for cases, when -
     * due to pagination - the query will result in more columns than originally
     * planned.
     *
     * @see org.melanesia.sql.Query#ignoreColumn(String)
     *
     * @param variation
     *            sql variation
     * @param columnName
     *            column name to ignore
     */
    protected final void ignoreColumn(final SQLVariation variation, final String columnName) {
        ignoredColumns.get(variation).add(columnName);
    }

    /**
     * Defines ignorability of given column for given query variation.
     *
     * @see #ignoreColumn(SQLVariation, String)
     *
     * @param variation
     *            sql variation
     * @param columnName
     *            column name to test
     * @return true if the column should be ignored
     */
    final boolean isColumnIgnored(final SQLVariation variation, final String columnName) {
        return ignoredColumns.get(variation).contains(columnName);
    }

    /**
     * Returns boundary type defined in current {@code rangeCallback}.
     *
     * @see RangeCallback
     * @return boundary type of the range defined in {@code rangeCallback}
     */
    protected final BoundaryType getBoundaryType() {
        if (rangeCallback.getStartRow() > 1 && rangeCallback.getStopRow() > -1) {
            return BoundaryType.RANGE;
        } else if (rangeCallback.getStartRow() > 1) {
            return BoundaryType.BOTTOM_ROWS;
        } else {
            return BoundaryType.TOP_ROWS;
        }
    }

    /**
     * This method should be overridden in order to prepare sql variations and
     * their parameters according to database specifications and boundary type
     * returned by {@link #getBoundaryType()}. The query variations should be
     * prepared for {@link SQLVariation#COUNT_QUERY} and
     * {@link SQLVariation#RANGED_QUERY}.
     *
     * The prepared query variations should be then stored inside
     * {@code PaginatinSupport} object using
     * {@link #setSql(SQLVariation, String)}, and
     * {@link #setParameter(SQLVariation, String, Object)}.
     */
    public abstract void prepare();

}
